package edu.kit.csl.pisa.ui;

/*
This file is part of the PISA Alignment Tool.

Copyright (C) 2013
Karlsruhe Institute of Technology
Cognitive Systems Lab (CSL)
Felix Stahlberg

PISA is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

PISA is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with PISA. If not, see <http://www.gnu.org/licenses/>.
*/

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import edu.kit.csl.pisa.io.Logger;

/**
 * This singleton class stores the configuration, which is normally
 * set by command line arguments or configuration file.
 * 
 * @see #processCommandLineArguments(String[])
 */
public class Configuration {
	
	/*
	 * File name of the default configuration file
	 */
	final static private String DEFAULT_CONFIGURATION_FILE = "pisa.conf";
	
	final static private String VERSION = "0.3";
	
	private final String CONFIG_FILE_EXTENSION = ".pisacfg";
	
	/*
	 * Singleton instance
	 */
	static private Configuration conf;
	
	/*
	 * Parameter lists by their datatypes
	 */
	private Map<String, Integer> intParams;
	private Map<String, Float> floatParams;
	private Map<String, String> stringParams;
	private Map<String, String> descriptions;
	
	/*
	 * List of already included configuration files 
	 */
	private Set<String> alreadyIncluded;
	
	/*
	 * Add an integer parameter.
	 */
	private void addParameter(String key, int defaultValue, String desc) {
		intParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add integer model parameter for both models.
	 */
	private void addModelParameter(String key, int defaultValue, String desc) {
		addParameter("model3" + key, defaultValue, desc);
		addParameter("model4" + key, defaultValue, "See 'model3" + key + "'");
	}
	
	/*
	 * Add a floating point parameter.
	 */
	private void addParameter(String key, float defaultValue, String desc) {
		floatParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add float model parameter for both models.
	 */
	private void addModelParameter(String key, float defaultValue,
			String desc) {
		addParameter("model3" + key, defaultValue, desc);
		addParameter("model4" + key, defaultValue, "See 'model3" + key + "'");
	}
	
	/*
	 * Add a string parameter.
	 */
	private void addParameter(String key, String defaultValue, String desc) {
		stringParams.put(key, defaultValue);
		descriptions.put(key, desc);
	}
	
	/*
	 * Add string model parameter for both models.
	 */
	private void addModelParameter(String key, String defaultValue,
			String desc) {
		addParameter("model3" + key, defaultValue, desc);
		addParameter("model4" + key, defaultValue, "See 'model3" + key + "'");
	}
	
	/*
	 * Private constructor.
	 */
	private Configuration() {
		descriptions = new HashMap<String, String>();
		intParams = new HashMap<String, Integer>();
		floatParams = new HashMap<String, Float>();
		stringParams = new HashMap<String, String>();
		alreadyIncluded = new HashSet<String>();
	}
	
	/**
	 * Restores the default configuration.
	 */
	public void restore() {
		descriptions.clear();
		intParams.clear();
		floatParams.clear();
		stringParams.clear();
		SimpleDateFormat formatter = new SimpleDateFormat("yy-MM-dd.HHmmss.");
		addParameter("initialModelPath", "",
				"REQUIRED! Path to the initial model, which is used to "
				+ "initialize the Model 3P parameters. If initialModelLoader is "
				+ "set to 'giza' or 'min-giza', this should be GIZA++' -o "
				+ "parameter (GIZA++' output prefix). If initialModelLoader is "
				+ "set to 'min-pisa', this should point to the alignment "
				+ "file directly.");
		addParameter("initialModelLoader", "giza",
				"Loading strategy for the initial model. \n"
				+ "'giza': The IBM Model 3 is loaded from the output files of "
					+ "a GIZA++ run, including the parameter files. Note that "
					+ "GIZA++ should have ran with the -nbest parameter.\n"
				+ "'min-giza': Only a GIZA++ alignment file is required. All "
					+ "Model 3P parameters are distributed evenly.\n"
				+ "'min-pisa:': Use a PISA alignment file. initialModelPath "
				    + "should point directly to the alignment file.");
		addParameter("configFile", "",
				"Configuration file (GIZA++ format: <key> <val>). Parameters "
				+ "in this file override values in the default configuration "
				+ "file. Command line arguments override all other settings. "
				+ "If this parameter is used within a config file, it has "
				+ "'include once' semantics.");
		addParameter("logLevel", "INFO",
				"Controls the amount of output. "
				+ "FATAL: Only fatal errors, "
				+ "ERROR: All errors, "
				+ "WARN: Warnings and errors, "
				+ "INFO: Notices, warnings and errors, "
				+ "DEBUG: Debug mode");
		addParameter( // Like GIZA++
				"outputPrefix",
				formatter.format(new Date()) + System.getProperty("user.name"),
				"Prefix which is prepended to all output files");
		addParameter("maxNeighborhoodSize", 200000,
				"Maximum number of neighbors over which to collect fractional "
				+ "counts. A lower value avoids memory consumption peaks.");
		addParameter("nThreads", 2,
				"Maximum number of threads.");
		addParameter("maxSrcSenLen", -1,
				"Maximum number of words in a source sentence. If this value "
				+ "is negative, the source sentence length is unlimited.");
		addParameter("maxTrgtSenLen", -1,
				"Maximum number of phonemes in a target sentence. If this value "
				+ "value is negative, the source sentence length is unlimited.");
		addParameter("maxFertility", 10,
				"Maximum fertility. This is how many French words "
				+ "can be generated by a single English word.");
		addParameter("maxWordLength", 11,
				"Maximum word length. This is how many French "
				+ "phonemes can be generated by a single French word.");
		addParameter("probSmooth", 1e-7f,
				"Probability smoothing factor and minimum possible probability"
				+ " value for a model parameter.");
		addParameter("model4MaxDistortion", 40,
				"Maximum distance between source word position and cept "
				+ "position. The probability mass for larger distortions are "
				+ "accumulated in a single cluster.");
		addParameter("model4SentenceLengthModel", "difference",
				"Sentence length model for Model 4P. Possible values are:\n"
				+ "'uniform': No model (uniform distribution over all possible"
					+" differences.\n"
				+ "'difference': ML estimate of P(#srcWords - #trgtWords)\n"
				+ "'negativeBinomial': Negative binomial distribution. r and p"
					+ " parameters are estimated using MLE for each source "
					+ "sentence length separately.\n"
				+ "'restrictedNegativeBinomial': Negative binomial distribution."
					+ " The r parameter is estimated using MLE for each source "
					+ "sentence length separately. The p parameter is chosen "
					+ "such that the mean of the resulting distribution is equal"
					+ " to the source sentence length.");
		addParameter("model4SentenceLengthModelWeight", 0.3f,
				"Weight w of the sentence length model. The Model 4P probability"
					+ " and the sentence length score are combined by "
					+ "P(Model4P)*P(length)^w");
		
		addModelParameter("Iterations", 5,
				"Number of iterations for Model 3P");
		addModelParameter("SuccStrategy", "viterbi",
				"This parameters defines the successor strategy for EM "
				+ "training. This strategy defines what are the starting "
				+ "points for maximation strategy instances. Possible values "
				+ "are:\n"
				+ "'id': the complete neighborhood of the last iteration is "
					+ " the initial population in the next iteration.\n"
				+ "'viterbi': Only the viterbi alignment is used in the next "
					+ "initial population, other individuals are random.\n"
				+ "'nBest': The legacySize best individuals will be used in "
					+ "next initial population.\n"
				+ "'random': legacySize arbitrary individuals (and the viterbi"
					+ " inidividual) are transferred to the next iteration.\n"
				+ "'no': No individuals are transferred. Note that this cannot"
					+ " be used in combination with locks.");
		addModelParameter("MaxStrategy", "eva2",
				"This parameter defines which maximization "
				+ "engine should be used. Possible values are: 'eva2' for "
				+ "using the EvA2 framework, 'id' for not using any "
				+ "optimization. The optimization"
				+ " engines may have additional sub-parameters.");
		addModelParameter("MaxAlgorithm", "STD_GA",
				"The local/global optimization algorithm used in the M-step of"
				+ " EM training. Possible values are:\n"
				+ "'STD_ES' A standard (15,50)-Evolution Strategy.\n"
				+ "'CMA_ES' (15,50)-Evolution Strategy with Covariance Matrix "
					+ "Adaptation.\n"
				+ "'STD_GA' Standard Genetic Algorithm with elitism.\n"
				+ "'PSO' Particle Swarm Optimization with constriction.\n"
				+ "'DE' Differential Evolution.\n"
				+ "'TRIBES' Tribes: an adaptive PSO.\n"
				+ "'RANDOM' Random search (Monte-Carlo).\n"
				+ "'HILLCL' Multi-start hill climbing.\n"
				+ "'CL_HILLCL' Clustering multi-start hill climbing.\n"
				+ "'SIMANN' Simulated Annealing.\n"
				+ "'CBN_ES' Clustering-based Niching ES.\n"
				+ "See the EvA2 docu for further information.");
		addModelParameter("MaxAlgorithmLegacyFraction", 0.5f,
				"This parameter defines the fraction of legacy individuals in "
				+ "population based optimization algorithms. For instance, a "
				+ "value of 0.75 means that the initial population of a "
				+ "optimization run consists of 25% random individuals and "
				+ "75% of individuals in the last generation of the last "
				+ "optimization run (former M-step in EM algorithm).");
		addModelParameter("MaxAlgorithmIterations", 8500,
				"Number of iterations of the maximization algorithm. For "
				+ "example, if using a GA, this would be the number of "
				+ "generations in each M-step for each sentence pair.");
		addModelParameter("MaxAlgorithmPopulationSize", 100,
				"Size of the population of the maximization algorithm.");
		addModelParameter("CrossoverProb", 0.1f,
				"Crossover probability in GA/ES optimization.");
		addModelParameter("MutationProb", 0.9f,
				"Mutation probability in GA/ES optimization.");
		addModelParameter("MutateIns", 1,
				"Mutation by inserting a word boundary (0 or 1).");
		addModelParameter("MutateDel", 1,
				"Mutation by removing a word boundary (0 or 1).");
		addModelParameter("MutateMove", 1,
				"Mutation by moving a word boundary one phoneme to "
				+ "the left or to the right (0 or 1).");
		addModelParameter("MutateRealign", 1,
				"Mutation by realigning a target word to a new "
				+ "source word (0 or 1).");
		addModelParameter("SmoothFactor", 0.2f,
				"Smoothing factor used when collecting fractional counts.");
		addModelParameter("ModelDumpFrequency", 1,
				"Each x-th iteration Model3P is written to the file system.");
		addModelParameter("CorpusDumpFrequency", 1,
				"Each x-th iteration while training Model3P, the corpus with "
				+ "the current viterbi alignments is written to the file "
				+ "system.");
	}
	
	/**
	 * Get configuration singleton. If it does not exist yet, create it and 
	 * load default configuration file.
	 * 
	 * @return configuration singleton
	 */
	static public Configuration getSingleton() {
		if (conf == null) {
			conf = new Configuration();
			conf.restore();
			conf.loadConfigurationFile(DEFAULT_CONFIGURATION_FILE);
		}
		return conf;
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, int value) {
		if (!intParams.containsKey(key)) {
			Logger.getSingleton().fatalError(
					"Unknown integer parameter " + key);
		} else {
			intParams.put(key, value);
		}
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, float value) {
		if (!floatParams.containsKey(key)) {
			Logger.getSingleton().fatalError(
					"Unknown floating point parameter " + key);
		} else {
			floatParams.put(key, value);
		}
	}
	
	/**
	 * Get a integer parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public Integer getInteger(String key) {
		return intParams.get(key);
	}
	
	/**
	 * Get a floating point parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public Float getFloat(String key) {
		return floatParams.get(key);
	}
	
	/**
	 * Get a string parameter. If the key is unknown, return null
	 * 
	 * @param key the name of the parameter
	 * @return the value of the parameter or null
	 */
	public String getString(String key) {
		return stringParams.get(key);
	}
	
	/**
	 * Set the value of a parameter.
	 * 
	 * @param key
	 * @param value
	 */
	public void set(String key, String value) {
		try {
			if (intParams.containsKey(key)) {
				set(key, Integer.parseInt(value));
			} else  if (floatParams.containsKey(key)){
				set(key, new Float(value));
			} else if (stringParams.containsKey(key)) {
				stringParams.put(key, value);
				if (key.equals("logLevel")) {
					Logger.getSingleton().setLevel(value);
				}
			} else {
				Logger.getSingleton().fatalError(
						"Unknown parameter " + key);
			}
		} catch(NumberFormatException e) {
			Logger.getSingleton().fatalError(
					"Parameter " + key + " should be numeric");
		}
	}
	
	/**
	 * Parse the command line arguments and overrides parameters if they
	 * appear in them.
	 * 
	 * @param argv string array with command line arguments
	 */
	public void processCommandLineArguments(String[] args) {
		String curParam = null;
		for (String arg : args) {
			if (arg.charAt(0) == '-') {
				if (curParam != null) {
					set(curParam, 1);
				}
				curParam = arg.substring(1);
			} else if (curParam != null) {
				set(curParam, arg);
				if (curParam.equals("configFile") 
						&& loadConfigurationFile(arg)) {
					// Process command line arguments again
					processCommandLineArguments(args);
					return;
				}
				curParam = null;
			} else {
				Logger.getSingleton().fatalError(
						"Malformed command line argument " + arg);
			}
		}
	}
	
	/**
	 * Load parameters from a configuration file. The file should be in GIZA++
	 * format (i.e. one parameter per line, key/value separated by a blank).
	 * Lines starting with '#' are ignored.
	 * 
	 * @param fileName path to the file to load
	 * @param boolean TRUE if the file could be loaded, FALSE otherwise
	 */
	public boolean loadConfigurationFile(String fileName) {
		if (alreadyIncluded.contains(fileName)) {
			return false;
		}
		Logger log = Logger.getSingleton();
		alreadyIncluded.add(fileName);
		try {
			FileInputStream fstream = new FileInputStream(fileName);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// Read File Line By Line
			while ((strLine = br.readLine()) != null) {
				if (strLine.charAt(0) == '#') { // Comment
					continue;
				}
				String[] split = strLine.split(" ", 2);
				if (split.length != 2) {
					log.error("Malformed line " + strLine + " in " + fileName);
				}
				set(split[0], split[1]);
			}
			in.close();
		} catch (Exception e) {// Catch exception if any
			log.error("Error while reading config file "
					+ fileName + ": " + e.getMessage());
			return false;
		}
		return true;
	}
	
	/**
	 * Prints a help text to stdout.
	 */
	public void printHelp() {
		System.out.println("PISA " + VERSION + " is a tool for cross-lingual "
				+ "word-to-phoneme alignment.");
		System.out.println(
				"Configuration via command line arguments: -<name> <value>");
		System.out.println(
				"Configuration via file (per line): <name> <value>");
		System.out.println("Following parameters are available.");
		System.out.println("");
		System.out.println("String parameters");
		System.out.println("---------------------");
		SortedSet<String> keys = new TreeSet<String>(stringParams.keySet());
		for (String key : keys) {
		   printParameter(key, stringParams.get(key),
					descriptions.get(key));
		}
		System.out.println("");
		System.out.println("Integer parameters");
		System.out.println("---------------------");
		keys = new TreeSet<String>(intParams.keySet());
		for (String key : keys) {
		   printParameter(key, intParams.get(key).toString(),
					descriptions.get(key));
		}
		System.out.println("");
		System.out.println("Floating point parameters");
		System.out.println("---------------------");
		keys = new TreeSet<String>(floatParams.keySet());
		for (String key : keys) {
		   printParameter(key, floatParams.get(key).toString(),
					descriptions.get(key));
		}
	}
	
	/**
	 * Builds the content of a config file for the current configuration.
	 */
	public void dumpToFilesystem() {
		String fileName = getString("outputPrefix") 
				+ CONFIG_FILE_EXTENSION;
		try {
			FileWriter fs = new FileWriter(fileName);
			BufferedWriter out = new BufferedWriter(fs);
			SimpleDateFormat formatter = new SimpleDateFormat("yy-MM-dd HH:mm:ss.");
			out.write("# Configuration for PISA run.\n");
			out.write("# PISA-Version: " + VERSION + "\n");
			out.write("# Start: " + formatter.format(new Date()) + "\n");
			out.write("# Working Directory: " + System.getProperty("user.dir")
					+ "\n");
			out.write("\n# String Parameters\n");
			SortedSet<String> keys = new TreeSet<String>(stringParams.keySet());
			for (String key : keys) {
				out.write(key + " " + stringParams.get(key) + "\n");
			}
			out.write("\n# Integer Parameters\n");
			keys = new TreeSet<String>(intParams.keySet());
			for (String key : keys) {
				out.write(key + " " + intParams.get(key) + "\n");
			}
			out.write("\n# Floating Point Parameters\n");
			keys = new TreeSet<String>(floatParams.keySet());
			for (String key : keys) {
				out.write(key + " " + floatParams.get(key) + "\n");
			}
			out.close();
		} catch (IOException e) {
			Logger.getSingleton().error("Error while dumping corpus to "
					+ fileName + ": " + e.getMessage());
		}
	}
	public void asConfigurationFile() {
		
	}
	
	/*
	 * Prints a single parameter to stdout.
	 */
	private void printParameter(String key, String value, String description) {
		if (value.equals("")) {
			value = "<not set>";
		}
		System.out.println(key + " (Default: " + value + ")");
		System.out.println(description);
		System.out.println("");
	}
}
